﻿using System;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Linq;

namespace Lapkin.Attributes
{
    [AttributeUsage(AttributeTargets.Property)]
    public class UniqueAttribute : ValidationAttribute
    {
        private readonly Type _contextType;
        public Type ContextType
        {
            get { return _contextType; }
        }
        private readonly string _memberName;
        public string MemberName
        {
            get { return _memberName; }
        }

        public UniqueAttribute(Type contextType, string memberName)
        {
            if (contextType == null)
                throw new ArgumentNullException("contextType");
            if (!contextType.IsSubclassOf(typeof (DbContext)) && !contextType.IsSubclassOf(typeof (ObjectContext)))
                throw new ArgumentException("The contextType Type must be a subclass of DbContext or ObjectContext.",
                                            "contextType");
            if (contextType.GetConstructor(Type.EmptyTypes) == null)
                throw new ArgumentException("The contextType type must declare a public parameterless consructor.");

            _contextType = contextType;
            _memberName = memberName;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (value == null || (value is string && string.IsNullOrWhiteSpace((string) value)))
                return ValidationResult.Success;

            var type = validationContext.ObjectType;
            var property = type.GetProperty(_memberName);
            type = property.DeclaringType;

            using (var dbcontext = (IDisposable) Activator.CreateInstance(_contextType))
            {
                var context = dbcontext is DbContext
                                  ? ((IObjectContextAdapter) dbcontext).ObjectContext
                                  : (ObjectContext) dbcontext;
                var md = context.MetadataWorkspace;
                var entityType = md.GetItems<EntityType>(DataSpace.CSpace).SingleOrDefault(et => et.Name == type.Name);

                while (entityType.BaseType != null)
                    entityType = (EntityType) entityType.BaseType;

                var objectType = typeof (object);
                var isInherited = false;
                var baseType = type;
                while (baseType.Name != entityType.Name && baseType.BaseType != objectType)
                {
                    baseType = baseType.BaseType;
                    isInherited = true;
                }

                var methodCreateObjectSet =
                    typeof (ObjectContext).GetMethod("CreateObjectSet", Type.EmptyTypes).MakeGenericMethod(baseType);
                var baseObjectSet = (ObjectQuery) methodCreateObjectSet.Invoke(context, new object[] {});
                var objectSet = baseObjectSet;
                var setType = baseObjectSet.GetType();

                if (isInherited)
                {
                    var ofType = setType.GetMethod("OfType");
                    ofType = ofType.MakeGenericMethod(type);
                    objectSet = (ObjectQuery) ofType.Invoke(baseObjectSet, null);
                    setType = objectSet.GetType();
                }

                var methodWhere = setType.GetMethod("Where");

                var eSql = string.Format("it.{0} = @{0}", _memberName);

                var query = (ObjectQuery) methodWhere.Invoke(objectSet,
                                                             new object[]
                                                                 {
                                                                     eSql,
                                                                     new[]
                                                                         {
                                                                             new ObjectParameter(
                                                                 _memberName, value)
                                                                         }
                                                                 });

                var result = query.Execute(MergeOption.NoTracking).Cast<object>();

                bool isValid = true;
                using (var enumerator = result.GetEnumerator())
                {
                    if (enumerator.MoveNext())
                    {
                        var nameProperty = typeof (ObjectSet<>).MakeGenericType(baseType).GetProperty("EntitySet");
                        var entitySet = (EntitySet) nameProperty.GetValue(baseObjectSet, null);
                        var entitySetName = entitySet.Name;

                        do
                        {
                            var current = enumerator.Current;
                            var curKey = context.CreateEntityKey(entitySetName, current);
                            var validatingKey = context.CreateEntityKey(entitySetName, validationContext.ObjectInstance);

                            if (curKey != validatingKey)
                            {
                                isValid = false;
                                break;
                            }
                        } while (enumerator.MoveNext());
                    }
                }

                return isValid
                           ? ValidationResult.Success
                           : new ValidationResult(ErrorMessage);
            }
        }
    }
}